use std::{collections::HashMap, fs, path::PathBuf};

use clap::{Parser, ValueEnum};
use serde_derive::Deserialize;

const LOWEST_PORT_NUMBER: u16 = 1;
const TOP_PORT_NUMBER: u16 = 65535;

#[derive(Deserialize,Debug,ValueEnum,Clone,Copy,PartialEq, Eq)]
pub enum ScanOrder {
    Serial,
    Random,
}
#[derive(Deserialize,Debug,ValueEnum,Clone,PartialEq, Eq,Copy)]
pub enum ScriptsRequired{
    None,
    Default,
    Custom,
}

#[derive(Deserialize,Debug,Clone,PartialEq, Eq)]
pub struct PortRange {
    pub start: u16,
    pub end: u16,
}

fn parse_range(input: &str) -> Result<PortRange,String> {
    let range = input
        .split('-')
        .map(str::parse)
        .collect::<Result<Vec<u16>, std::num::ParseIntError>>();
    if range.is_err() {
        return Err(String::from("the range format must be 'start-end'. Example: 1-1000."));
    }

    match range.unwrap().as_slice() {
        [start,end] => Ok(PortRange { start: *start, end: *end }),
        _ => Err(String::from("the range format must be 'start-end'. Example: 1-1000.")),
    }
}

#[derive(Parser,Debug,Clone)]
#[command(
    name = "zscaner",
    version = env!("CARGO_PKG_VERSION"),

)]
pub struct Opts{
    pub addresses: Vec<String>,
    pub ports: Option<Vec<u16>>,
    #[arg(short,long,conflicts_with = "ports",value_parser = parse_range)]
    pub range: Option<PortRange>,
    pub no_config: bool,
    pub config_path: Option<PathBuf>,
    pub greppable: bool,
    pub accessible: bool,
    pub resolver: Option<String>,
    pub batch_size: u16,
    pub timeout: u32,
    pub tries: u8,
    pub ulimit: Option<u64>,
    
    #[arg(long,value_enum,ignore_case= true,default_value="serial")]
    pub scan_order: ScanOrder,
    pub scripts: ScriptsRequired,
    pub top: bool,
    pub command: Vec<String>,
    pub exclude_ports: Option<Vec<u16>>,
    pub udp: bool,
}

impl Opts {
    pub fn read() ->Self{
        let mut opts:Opts = Opts::parse();
        if opts.ports.is_none() && opts.range.is_none() {
            opts.range = Some(PortRange { start: LOWEST_PORT_NUMBER, end: TOP_PORT_NUMBER, });
        }

        opts
    }

    pub fn merge(&mut self,config: &Config){
        if !self.no_config {
            self.merge_required(config);
            self.merge_optional(config);
        }
    }

    fn merge_required(&mut self,config: &Config){
        macro_rules! merge_required {
            ($($field: ident),+) => {
                $(
                    if let Some(e) = &config.$field {
                        self.$field = e.clone();
                    }
                )+
            }
        }
        merge_required!(addresses,greppable,accessible,batch_size,timeout,tries,scan_order,scripts,command,udp);
    }
    fn merge_optional(&mut self,config: &Config){
        macro_rules! merge_optional {
            ($($field: ident),+) => {
                $(
                    if config.$field.is_some(){
                        self.$field = config.$field.clone();
                    }
                )+
            }
        }
        if self.top && config.ports.is_some() {
            let mut ports: Vec<u16> = Vec::with_capacity(config.ports.as_ref().unwrap().len());
            for entry in config.ports.as_ref().unwrap().keys() {
                ports.push(entry.parse().unwrap());
            }
            self.ports = Some(ports);
        }
        merge_optional!(range,resolver,ulimit,exclude_ports);
    }
}

impl Default for Opts {
    fn default() -> Self {
        Self { 
            addresses: vec![],
            ports: None, 
            range: None, 
            greppable: true,
            batch_size: 0,
            timeout: 0,
            tries: 0,
            ulimit: None,
            command: vec![],
            accessible: false,
            resolver: None,
            scan_order: ScanOrder::Serial,
            no_config: true, 
            top: false,
            scripts: ScriptsRequired::Default,
            config_path: None, 
            exclude_ports : None,
            udp: false,
        }
    }
}


#[derive(Debug,Deserialize)]
pub struct Config{
    addresses: Option<Vec<String>>,
    ports: Option<HashMap<String,u16>>,
    range: Option<PortRange>,
    greppable: Option<bool>,
    accessible: Option<bool>,
    batch_size: Option<u16>,
    timeout: Option<u32>,
    tries: Option<u8>,
    ulimit: Option<u64>,
    resolver: Option<String>,
    scan_order: Option<ScanOrder>,
    command: Option<Vec<String>>,
    scripts: Option<ScriptsRequired>,
    exclude_ports: Option<Vec<u16>>,
    udp: Option<bool>,
}

impl Config {
    pub fn read(custom_config_path: Option<PathBuf>) -> Self {
        let mut content:String = String::new();
        let config_path = custom_config_path.unwrap_or_else(default_config_path);
        if config_path.exists() {
            content = match fs::read_to_string(config_path) {
                Ok(content) => content,
                Err(_) => String::new(),
            }
        }

        let config: Config = match toml::from_str(&content) {
            Ok(config) => config,
            Err(e) =>   {
                println!("Found {e} in configuration file.\nAborting scan.\n");
                std::process::exit(1);
            }
        };

        config
    }
}

pub fn default_config_path() -> PathBuf {
    let Some(mut config_path) = dirs::home_dir() else{panic!("Could not infer config file path.")};
    config_path.push(".zscaner.toml");
    config_path
}